19.1 模块
说明:模块
是一组功能(服务)的集合,其中一些功能可以被程序的其它部分(客户
)使用。每个模块都有一个接口
来描述所提供的功能。模块的细节,包括这些功能自身的源代码,都包含在模块的实现
中。
文件角色 | 说明 |
---|---|
接口 | 模块名.h (头文件) |
实现 | 对应头文件模块名.c 文件 |
客户 | 通过#include 引入模块的文件 |
优点:将程序分割成模块有一系列好处
- 抽象:不必了解功能的实现细节,只需对模块的接口达成一致
- 可复用:每个模块都可以在另一个程序中复用
- 可维护性(最重要):当程序出现错误或需要升级时,通常只会影响一个模块
扩展:Fundamental of SoftWare Engineer
19.1.1 内聚性与耦合性
说明:一个好的模块接口并不是随意的一组声明,应具有下面两个性质
- 高内聚:模块中的元素应该紧密相关
- 低耦合:模块之间应该尽可能相互独立
19.1.2 模块的类型
类型 | 说明 | 举例 | 备注 |
---|---|---|---|
数据池 | 一些相关的变量或常量的集合 | <float.h> (23.1)、 <limits.h> (23.2) |
通常不建议将变量放在头文件 |
库 | 一组相关函数的集合 | <string.h> |
|
抽象对象 | 对隐藏的数据结构进行操作的一组函数的集合 | 如果数据是隐藏起来的,那么这个对象那个就是“抽象的” | |
抽象数据类型 | 将具体数据实现方式隐藏起来的数据类型称为抽象数据类型 | 抽象数据类型在当今的程序设计中起着非常重要的作用 |
19.2 信息隐藏
说明:一个设计良好的模块经常会对它的客户隐藏一些信息。谨慎的对客户隐藏信息的方法称为信息隐藏。
优点:
- 安全性:
客户
只能通过模块
自身的函数进行参数 - 灵活性:修改模块通常不必修改接口,对
客户
没有影响
原理:使用static
修饰 | 链接情况 | 说明 |
---|---|---|
函数 | 内部链接 | 只能在只能被同一文件中被调用 |
变量(文件作用域) | 内部链接 | 只能被同一文件中的其它函数访问 |
技巧:使用宏定义“公有”和“私有”可以使程序含义更加清晰(尤其是static
,因为它在c语言中有许多用法)
栈模块(实现部分)
1 |
|
19.3 抽象数据类型
说明:c语言没有设计专门用于封装类型的特性(class
),即无法定义真正的抽象数据类型。
案例缺陷:之前定义的栈模块提供的栈不基于一种抽象数据类型,而是仅仅提供了一个相当于“栈的实例”的数据结构。当需要多个栈实例时就无能为力了。
stack.h
1 | /** |
客户
1 |
|
19.4 C++语言
说明:C++语言是由AT&T
贝尔实验室的Bjqrne Stroustrup在20世纪80年代开发出来的C语言的扩展版。
新特性(相对C):
- 面向对象:允许从已经存在的类“派生”(继承)出新的类
- 运算符重载:为传统的C语言的运算符赋予新的含义
- 模版:可以使我们写出通用的、高度可复用的类和函数
- 异常处理:一种同一的方式用来检测并响应错误
兼容C:C++语言包含了标准C的全部特性,然而不是所有C语言都可以在C++的环境下编译,因为C++语言增加了更多强制限制,比C语言更加安全。
19.4.1 C语言与C++语言之间的差异
19.4.1.1 注释
新特性:C++语言支持单行注释
1 | // This is a Comment |
19.4.1.2 标记与类型名
新特性:标记(结构、联合或枚举的名字)会自动被认为是类型名
1 | struct Complex { |
19.4.1.3 不带参数的函数
新特性:在声明或定义一个不带参数的C++
函数时,可以不使用void
1 | void draw (void); // 不带参数 |
19.4.1.4 默认实际参数
新特性:C++语言允许函数的实际参数有默认值
1 | // 显示任意数量的换行符,如果没有提供参数,默认为1 |
19.4.1.5 引用参数
新特性:允许实际参数被声明为引用
,而不是指针
。
1 | // c语言的方式 |
19.4.1.6 动态存储分配
新特性:使用运算符new
(分配空间)、 delete
(释放空间)
语法:
- 分配内存:
new 类型说明符
- 释放内存:
delete 指针
1 | // 声明变量 |
19.4.2 类
类:一个类从根本上说就是一个抽象数据类型
:一组数据以及操作这些数据的函数
说明:这个新数据类型的功能可以同基本数据类型同样强大。
类的不足:类的设计和实现比较复杂,这是易用性必须付出的代价,而这也是计算机领域近几年内的妥协。
19.4.3 类定义
类标记(class tag):可以直接作为类型名使用,不要求类名以大写开始,但许多C++程序员尊循首字母大写的规范。
数据成员(data memeber):类似结构的成员(但是默认是隐藏的,即“私有的”)
类的实例(instance):任何类的实例就是对象
(object)
访问成员:使用运算符.
或->
来访问公有的成员
1 | class Fraction { |
19.4.4 成员函数
成员函数(member function):属于类的函数称为成员函数,特别的,那些需要访问类的私有数据成员的函数必须声明在类里面。
1 | class Fraction { |
1 | Fraction f1; |
19.4.5 构造函数
构造函数(controctor):
- 通常时自动调用的(编译器安排在合适的时机自动调用)
- 定义在public成员部分,不需要指定返回值类型
- 命名和类名相同
1 | class Fraction { |
19.4.6 构造函数和动态存储分配
说明:构造函数和析构函数为那些内存分配和回收函数提供了比较合适的时机。
举例:创建自己的String
类型
比较 | C++的String (自定义) |
C语言实现方式(char 数组) |
---|---|---|
字符串长度 | 任意长度 | 受限于数组的长度 |
获取字符串长度 | O(1) | O(n) |
扩展性 | 需要时可给String类添加操作 | 无法修改(就算引入 |
1 | class String{ |
19.4.7 析构函数
析构函数(destructor):
- 名字:
~类名
- 返回值:没有返回值
- 参数:没有
用途:自动存储期限的类的实例,当其生存期结束后,普通成员的内存会被释放,但在构造函数中分配内存的成员指向的内存不会被释放(内存泄漏)。所以需要析构函数在对象释放时自动调用,清理构造函数动态分配的内存。
1 | class String{ |
19.4.8 重载
函数重载
说明:在同一作用域下存在两个或以上同名但参数不同的函数(包括构造函数)叫做函数的重载。
用途:需要记住更少的函数名,编译器会根据实际参数的情况自动判断调用哪一个函数。
默认构造函数:不带时机参数的构造函数,会在声明对象而没有制定初始值时被调用。
1 | class Sring { |
1 | String s; // 不带实际参数,会调用默认构造函数 |
运算符重载
说明:重载运算符后,根据操作数类型的不同,同样的运算符号可以代表不同的操作。
用途:更易读,更自然(不需要定义一些难以记住的函数名)
1 | class Fraction { |
1 | ... |